﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using Newtonsoft.Json;
using System.Threading;
using System.Web;
using System.Net.WebSockets;
using System.Net;
using System.Diagnostics;//AddAddress方法使用

#region "服务端程序使用前配置说明"
/*
 *注意事项：在一台电脑上部署服务端时，以局域网为例，首先要保证客户端服务端电脑之间能够ping通。需要设置 允许网络发现，乃至关闭防火墙。
 *开启端口监听 httpListener.Start(); 此句很可能会报错，"system.net.httplistenerexception拒绝访问"。
 *此错误会导致客户端报system.net.websockets.websocketexception（无法连接远程服务器）异常，（有时服务端以127.0.0.1可以打开，但其实还是无法连接）
 *有三个解决方法：
 *1.服务端程序以管理员身份运行
 *
 *2.参考 https://blog.csdn.net/chenludaniel/article/details/79720024
 * 1) 以管理员权限打开CMD命令行
 * 2) 输入 netsh http show urlacl查看http://192.168.1.11:8081/在不在里面
 * 3) 先删除可能存在的错误urlacl，这里的*号代指localhost、127.0.0.1、192.168.199.X本地地址和+号等。
 *      命令：netsh http delete urlacl url=http://*:8081/
 *      这边使用： netsh http delete urlacl url=http://10.206.14.152:8080/
 * 4) 将上面删除的地址重新加进url，user选择所有人
 *      命令：netsh http add urlacl url=http://*:8080/  user=Everyone
 *      这边使用：netsh http add urlacl url=http://10.206.14.152:8080/  user=Everyone
 * 5) 配置防火墙。 可以用下面命令来配置
 *      netsh advfirewall firewall Add rule name=\"命令行Web访问8081\" dir=in protocol=tcp localport=8081 action=allow
 *      或者通过防火墙界面的 入站规则 里面添加一个8081的TCP端口
 * 
 * 3.将2中操作写为C#代码，在catch错误后执行
 * 参考：https://www.cnblogs.com/cmdszh/archive/2012/08/16/httplistener.html
 *      https://www.cnblogs.com/Leo_wl/p/3356288.html
 * 1) 生成manifest文件并配置UAC权限。项目属性--安全性--启用clickonce安全设置。会在项目的“Properties”目录下生成app.manifest文件。
 *      使用requireAdministrator账户控制级别，替换节点中asInvoker。(这是程序运行前提升，即程序一运行就提升管理员权限)
 * 2) 编写AddAddress方法将操作变成代码
 * 3) 使用try-catch
 * 注：此方法实际使用有问题，无法为manifest文件中节点设置管理员身份。
 * 
 * 另：第三方工具如fleck，会自动为调用的程序创建入站规则等。
 */
#endregion

namespace WinformServer
{
    public partial class frmServer : Form
    {
        static HttpListener httpListener = new HttpListener();//实例化一个静态httplistener对象
        public frmServer()
        {
            InitializeComponent();
        }

        #region "控件事件"
        //修改监听地址
        private void btnModifyIP_Click(object sender, EventArgs e)
        {
            if (btnModifyIP.Text == "修改")
            {
                txtIPAddress.Enabled = true;
                btnModifyIP.Text = "确定";
            }
            else if (btnModifyIP.Text == "确定")
            {
                txtIPAddress.Enabled = false;
                btnModifyIP.Text = "修改";
            }
        }
        //开启监听
        private void btnStart_Click(object sender, EventArgs e)
        {
            if (txtIPAddress.Enabled == true)
            {
                MessageBox.Show("请先确认地址");
                return;
            }
            if (httpListener.IsListening)//如果处于监听状态
            {
                MessageBox.Show("请先关闭当前已有监听，再开启一个新的监听");
                txtInfo.AppendText("请先关闭当前已有监听，再开启一个新的监听." + "httplistening:" + httpListener.IsListening.ToString() + DateTime.Now.ToString() + "\n");
                return;
            }
            string IpAdress = txtIPAddress.Text;
            txtInfo.AppendText("打开监听" + DateTime.Now.ToString() + "\n");
            Start(IpAdress);
        }
        //关闭监听
        private void btnClose_Click(object sender, EventArgs e)
        {
            if (httpListener.IsListening)
            {
                try
                {
                    httpListener.Stop();
                    txtInfo.AppendText("成功关闭" + DateTime.Now.ToString() + "\n");
                    lblListen.Text = "Closed";
                }
                catch (Exception ex)
                {
                    txtInfo.AppendText(ex.ToString() + DateTime.Now.ToString() + "\n");
                }
            }
            else
            {
                txtInfo.AppendText("此时httplistener并未处于监听状态，无法关闭"+ DateTime.Now.ToString() + "\n");
                lblListen.Text = "Closed";
                return;
            }
        }

        //服务器主动广播
        private void btnBroadcast_Click(object sender, EventArgs e)
        {
            string jsonmessage = SerializeJson(txtAvalue.Text, txtBvalue.Text);
            txtInfo.AppendText("进行了广播" + DateTime.Now.ToString() + "\n");
            txtInfo.AppendText("内容：" + jsonmessage + "\n");
            Broadcast(jsonmessage);
        }
        //打印出服务端连接静态列表中所有元素
        private void btnShowConnections_Click(object sender, EventArgs e)
        {
            int ConnectionCount = _sockets.Count;
            txtInfo.AppendText("服务端当前存储了" + ConnectionCount + "个客户端连接:\n"+ DateTime.Now.ToString() + "\n");
            foreach (var innersocket in _sockets)
            {
                txtInfo.AppendText(innersocket.GetHashCode().ToString() + innersocket.State.ToString() + "\n");
            }
        }
        //提示以管理员运行
        private void frmServer_Load(object sender, EventArgs e)
        {
            txtInfo.AppendText("除非手动用netsh命令对服务端地址进行了配置，本程序需要以管理员身份运行，若没有，请重启\n");
            txtInfo.AppendText("另外，监听的端口请确认关闭了防火墙，或为端口进行了入站规则配置\n");

        }
        #endregion





        #region "服务端处理"
        //存储当前所有连接的静态列表
        private static List<WebSocket> _sockets = new List<WebSocket>();

        /// <summary>
        /// 对参数地址进行监听，如果是websocket请求，转入相应方法
        /// </summary>
        /// <param name="httpListenerPrefix">http监听地址，记得以 / 结尾</param>
        public async void Start(string httpListenerPrefix)
        {
            if (httpListener.IsListening)//先判断下是否已经开启了监听，否则如果进入后续代码，会一直出现与已有注册冲突报错，只能重启程序。
            {
                txtInfo.AppendText("请先关闭当前已有监听，再开启一个新的监听." + "httplistening:" + httpListener.IsListening.ToString() + DateTime.Now.ToString() + "\n");
                return;
            }
            try
            {
                //假如上一次监听由于地址错误等原因失败，重新监听时会报错对象已经被释放，因此每次监听前重新初始化对象
                httpListener = new HttpListener();
                httpListener.Prefixes.Add(httpListenerPrefix);
                //通过连接名称可以区分多个websocket服务。如可以通过 http://localhost:8080/learn http://localhost:8080/work 使用两个服务，不过需要多线程和两个http监听对象等
                httpListener.Start(); 
                lblListen.Text = "listening...";
                while (true)
                {
                    //http端口监听获取内容
                    HttpListenerContext httpListenerContext = await httpListener.GetContextAsync();
                    if (httpListenerContext.Request.IsWebSocketRequest)//如果是websocket请求
                    {
                        //进入此方法
                        ProcessRequest(httpListenerContext);
                    }
                    else
                    {
                        httpListenerContext.Response.StatusCode = 400;
                        httpListenerContext.Response.Close();
                    }
                }
            }
            catch (Exception ex)
            {
                //如果出现拒绝访问错误，则先将地址添加，然后提示程序重启。由于无法提升权限，此部分代码无用，暂且保留。
                //if (ex.GetType().ToString() == "System.Net.HttpListenerException")
                //{
                //    //AddAddress(httpListenerPrefix, Environment.UserDomainName, Environment.UserName);//没用
                //    txtInfo.AppendText(ex.ToString() + DateTime.Now.ToString() + "\n");
                //}
                //else
                //{
                //    txtInfo.AppendText(ex.ToString() + DateTime.Now.ToString() + "\n");
                //}
                txtInfo.AppendText(ex.ToString() + DateTime.Now.ToString() + "\n");
                //MessageBox.Show(ex.ToString());//暂且注释，弹出消息框影响观感
            }                     
        }

        /// <summary>
        /// 处理端口监听到的请求
        /// </summary>
        /// <param name="httpListenerContext"></param>
        private async void ProcessRequest(HttpListenerContext httpListenerContext)
        {
            //WebSocketContext 类用于访问websocket握手中的信息
            WebSocketContext webSocketContext = null;
            try
            {
                webSocketContext = await httpListenerContext.AcceptWebSocketAsync(subProtocol: null);
                //获取客户端IP
                string ipAddress = httpListenerContext.Request.RemoteEndPoint.Address.ToString();
                txtInfo.AppendText("connected:IPAddress" + ipAddress + "\n");
            }
            catch (Exception e)//如果出错
            {
                httpListenerContext.Response.StatusCode = 500;
                httpListenerContext.Response.Close();
                txtInfo.AppendText("Exception:" + e.ToString() + DateTime.Now.ToString() + "\n");
                return;
            }
            //获取websocket连接
            WebSocket webSocket = webSocketContext.WebSocket;
            _sockets.Add(webSocket);//此处将web socket对象加入一个静态列表中
            SendToNewConnection(webSocket);//将当前服务器上最新的数据（a,b的值）发送过去

            try
            {
                //我们定义一个常数，它将表示接收到的数据的大小。 它是由我们建立的，我们可以设定任何值。 我们知道在这种情况下，发送的数据的大小非常小。
                const int maxMessageSize = 2048;
                //received bits的缓冲区

                while (webSocket != null && webSocket.State == WebSocketState.Open)//如果连接是打开的
                {   
                    //此句放在while里面，每次使用都重新初始化。如果放在外面，由于没有进行清空操作，下一次接收的数据若比上一次短，则会多出一部分内容。
                    var receiveBuffer = new ArraySegment<Byte>(new Byte[maxMessageSize]);

                    WebSocketReceiveResult receiveResult=null;
                    byte[] payloadData = null;
                    do
                    {
                        //读取数据。此类的实例表示在 WebSocket 上执行单个 ReceiveAsync 操作所得到的结果
                        receiveResult = await webSocket.ReceiveAsync(receiveBuffer, CancellationToken.None);
                        //字节数组
                        payloadData = receiveBuffer.Array.Where(b => b != 0).ToArray();
                    }
                    while (!receiveResult.EndOfMessage);//如果指示已完整接收消息则停止
                    
                    //如果输入帧为取消帧，发送close命令。
                    //MessageType指示当前消息是utf-8消息还是二进制信息。Text(0,明文形式),Close(2，收到关闭消息，接受已完成),Binary(1,消息采用二进制格式)
                    if (receiveResult.MessageType == WebSocketMessageType.Close)
                    {
                        await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, String.Empty, CancellationToken.None);
                        _sockets.Remove(webSocket);//从列表移除当前连接

                    }
                    else
                    {                        
                        //因为我们知道这是一个字符串，我们转换它                        
                        string receiveString = System.Text.Encoding.UTF8.GetString(payloadData, 0, payloadData.Length);

                        try//将反序列化内容放入try中，避免无法匹配、内容为空等可能报错的地方
                        {
                            //将转换后的字符串内容进行json反序列化。参考：https://www.cnblogs.com/yinmu/p/12160343.html
                            TestValue tv = JsonConvert.DeserializeObject<TestValue>(receiveString);
                            //将收到的a,b的值显示到文本框
                            if (tv != null)
                            {
                                string valueA = string.Empty, valueB = string.Empty;
                                if (tv.a != null && tv.a.Length > 0) { valueA = tv.a; }
                                if (tv.a != null && tv.b.Length > 0) { valueB = tv.b; }
                                txtAvalue.Text = valueA;
                                txtBvalue.Text = valueB;
                            }
                            
                            RefreshConnectionList();//先清理无效的连接，否则会导致服务端websocket被dispose
                            //当接收到文本消息时，对当前服务器上所有web socket连接进行广播
                            foreach (var innerSocket in _sockets)
                            {
                                await innerSocket.SendAsync(new ArraySegment<byte>(payloadData), WebSocketMessageType.Text, true, CancellationToken.None);
                            }
                        }
                        catch (Exception ex)
                        {
                            //如果json反序列化出了问题
                            txtInfo.AppendText(ex.ToString() + DateTime.Now.ToString() + "\n");//将错误类型显示出来
                            txtInfo.AppendText(receiveString + DateTime.Now.ToString() + "\n");//将收到的原始字符串显示出来
                        }
                    }
                }
            }
            catch (Exception e)
            {
                if (e.GetType().ToString() == "System.Net.WebSockets.WebSocketException")
                {
                    //客户端关闭时会抛出此错误
                    txtInfo.AppendText("a connection closed" + DateTime.Now.ToString() + "\n");
                }
                else
                {
                    txtInfo.AppendText(e.ToString() + DateTime.Now.ToString() + "\n");
                }
            }
        }

        /// <summary>
        /// 服务端主动向所有客户端广播
        /// </summary>
        /// <param name="jsonmessage">传过来的应该是序列化后的json字符串，接收方会通过TestValue类进行反序列化获取a,b的内容</param>
        public  async void Broadcast(string jsonmessage)
        {
            try
            {
                Byte[] bytes = System.Text.Encoding.UTF8.GetBytes(jsonmessage);
                RefreshConnectionList();//先清理无效的连接，否则会导致服务端websocket被dispose
                //当接收到文本消息时，对当前服务器上所有web socket连接进行广播
                foreach (var innerSocket in _sockets)
                {                
                    await innerSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None);
                }
            }
            catch (Exception ex)
            {
                txtInfo.AppendText(ex.ToString() + DateTime.Now.ToString() + "\n");
                MessageBox.Show("某些连接出了问题，如果广播多次出问题，请重启服务端");
            }
            
        }

        /// <summary>
        /// 监听到一个新的websocket连接后，将服务器当前最新数据同步过去
        /// </summary>
        /// <param name="currentWebsocket">当前新接入的websocket连接</param>
        public async void SendToNewConnection(WebSocket currentWebsocket)
        {
            try
            {
                string jsonmessage = SerializeJson(txtAvalue.Text, txtBvalue.Text);
                Byte[] bytes = System.Text.Encoding.UTF8.GetBytes(jsonmessage);

                if (currentWebsocket.State == WebSocketState.Open)
                {
                    await currentWebsocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, CancellationToken.None);//try访问已释放对象问题
                }
                else
                {
                    //此处并不对该链接进行移除，会导致调用本方法后的代码出问题，只需在进行发送时确认状态即可
                    txtInfo.AppendText("新接入连接:" + currentWebsocket.GetHashCode().ToString() + "的连接状态不为open，因此停止向其同步数据" + DateTime.Now.ToString() + "\n");
                }
            }
            catch (Exception ex)
            {
                txtInfo.AppendText(ex.ToString() + DateTime.Now.ToString() + "\n");
            }
        }

        /// <summary>
        /// //以dictionary将数据的键值对匹配，然后进行json序列化，避免定义类的麻烦。
        /// </summary>
        /// <param name="valueA">a的值</param>
        /// <param name="valueB">b的值</param>
        /// <returns></returns>
        public static string SerializeJson(string valueA, string valueB)
        {
            if (valueA.Length == 0) { valueA = "-"; }
            if (valueB.Length == 0) { valueB = "-"; }
            //以dictionary将数据的键值对匹配，然后进行json序列化，避免定义类的麻烦。参考：https://www.cnblogs.com/kevinWu7/p/10163455.html
            Dictionary<string, string> dic = new Dictionary<string, string>(){
                { "a",valueA },
                { "b",valueB }
            };
            string Jsondata = JsonConvert.SerializeObject(dic);
            return Jsondata;
        }

        /// <summary>
        /// 刷新当前websocket连接列表，如果状态为Closed，则移除。连接异常断开后会被dispose，如果访问会报错，但可以获取状态为closed
        /// </summary>
        public static void RefreshConnectionList()
        {
            if (_sockets != null)//lock不能锁定空值
            {
                lock (_sockets)//锁定数据源
                {
                    //System.InvalidOperationException: 集合已修改；可能无法执行枚举操作。
                    //使用foreach不能执行删除、修改，这是规定。你可以使用for循环遍历修改。删除数据正确做法，for循环 i 要从大到小
                    for (int i = _sockets.Count-1; i >=0; i--)
                    {
                        if (_sockets[i].State != WebSocketState.Open)
                        {
                            _sockets.Remove(_sockets[i]);
                        }
                    }
                }
            }
            
        }

        /// <summary>
        /// 代码代替命令行操作，添加服务端地址。由于无法向manifest文件中设置管理员权限，此方法已经不用
        /// </summary>
        /// <param name="address">要添加的地址，如http://10.206.14.152:8080/</param>
        /// <param name="domain">计算机名</param>
        /// <param name="user">计算机账户</param>
        public static void AddAddress(string address, string domain, string user)
        {
            string argsDll = String.Format(@"http delete urlacl url={0}", address);
            string args = string.Format(@"http add urlacl url={0} user={1}\{2}", address, domain, user);
            ProcessStartInfo psi = new ProcessStartInfo("netsh", argsDll);
            psi.Verb = "runas";
            psi.CreateNoWindow = true;
            psi.WindowStyle = ProcessWindowStyle.Hidden;
            psi.UseShellExecute = false;
            Process.Start(psi).WaitForExit();//删除urlacl
            psi = new ProcessStartInfo("netsh", args);
            psi.Verb = "runas";
            psi.CreateNoWindow = true;
            psi.WindowStyle = ProcessWindowStyle.Hidden;
            psi.UseShellExecute = false;
            Process.Start(psi).WaitForExit();//添加urlacl
        }
        #endregion "服务端处理"



        

        

        

        



    }

    //用于json反序列化获取实体
    public class TestValue
    {
        public string a { get; set; }
        public string b { get; set; }
    }
}
